<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=783129
-->
<head>
  <title>Test for document.registerElement lifecycle callback</title>
  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=783129">Bug 783129</a>
<div id="container">
  <x-hello id="hello"></x-hello>
  <button id="extbutton" is="x-button"></button>
</div>
<script>

var container = document.getElementById("container");

// Tests callbacks after registering element type that is already in the document.
// create element in document -> register -> remove from document
function testRegisterUnresolved() {
  var helloElem = document.getElementById("hello");

  var createdCallbackCalled = false;
  var attachedCallbackCalled = false;
  var detachedCallbackCalled = false;

  var p = Object.create(HTMLElement.prototype);
  p.createdCallback = function() {
    is(helloElem.__proto__, p, "Prototype should be adjusted just prior to invoking the created callback.");
    is(createdCallbackCalled, false, "Created callback should only be called once in this tests.");
    is(this, helloElem, "The 'this' value should be the custom element.");
    createdCallbackCalled = true;
  };

  p.attachedCallback = function() {
    is(createdCallbackCalled, true, "Created callback should be called before attached");
    is(attachedCallbackCalled, false, "attached callback should only be called once in this test.");
    is(this, helloElem, "The 'this' value should be the custom element.");
    attachedCallbackCalled = true;
  };

  p.detachedCallback = function() {
    is(attachedCallbackCalled, true, "attached callback should be called before detached");
    is(detachedCallbackCalled, false, "detached callback should only be called once in this test.");
    detachedCallbackCalled = true;
    is(this, helloElem, "The 'this' value should be the custom element.");
    runNextTest();
  };

  p.attributeChangedCallback = function(name, oldValue, newValue) {
    ok(false, "attributeChanged callback should never be called in this test.");
  };

  document.registerElement("x-hello", { prototype: p });
  is(createdCallbackCalled, true, "created callback should be called when control returns to script from user agent code");

  // Remove element from document to trigger detached callback.
  container.removeChild(helloElem);
}

// Tests callbacks after registering an extended element type that is already in the document.
// create element in document -> register -> remove from document
function testRegisterUnresolvedExtended() {
  var buttonElem = document.getElementById("extbutton");

  var createdCallbackCalled = false;
  var attachedCallbackCalled = false;
  var detachedCallbackCalled = false;

  var p = Object.create(HTMLButtonElement.prototype);
  p.createdCallback = function() {
    is(buttonElem.__proto__, p, "Prototype should be adjusted just prior to invoking the created callback.");
    is(createdCallbackCalled, false, "Created callback should only be called once in this tests.");
    is(this, buttonElem, "The 'this' value should be the custom element.");
    createdCallbackCalled = true;
  };

  p.attachedCallback = function() {
    is(createdCallbackCalled, true, "Created callback should be called before attached");
    is(attachedCallbackCalled, false, "attached callback should only be called once in this test.");
    is(this, buttonElem, "The 'this' value should be the custom element.");
    attachedCallbackCalled = true;
  };

  p.detachedCallback = function() {
    is(attachedCallbackCalled, true, "attached callback should be called before detached");
    is(detachedCallbackCalled, false, "detached callback should only be called once in this test.");
    detachedCallbackCalled = true;
    is(this, buttonElem, "The 'this' value should be the custom element.");
    runNextTest();
  };

  p.attributeChangedCallback = function(name, oldValue, newValue) {
    ok(false, "attributeChanged callback should never be called in this test.");
  };

  document.registerElement("x-button", { prototype: p, extends: "button" });
  is(createdCallbackCalled, true, "created callback should be called when control returns to script from user agent code");

  // Remove element from document to trigger detached callback.
  container.removeChild(buttonElem);
}

function testInnerHTML() {
  var createdCallbackCalled = false;

  var p = Object.create(HTMLElement.prototype);
  p.createdCallback = function() {
    is(createdCallbackCalled, false, "created callback should only be called once in this test.");
    createdCallbackCalled = true;
  };

  document.registerElement("x-inner-html", { prototype: p });
  var div = document.createElement(div);
  div.innerHTML = '<x-inner-html></x-inner-html>';
  is(createdCallbackCalled, true, "created callback should be called after setting innerHTML.");
  runNextTest();
}

function testInnerHTMLExtended() {
  var createdCallbackCalled = false;

  var p = Object.create(HTMLButtonElement.prototype);
  p.createdCallback = function() {
    is(createdCallbackCalled, false, "created callback should only be called once in this test.");
    createdCallbackCalled = true;
  };

  document.registerElement("x-inner-html-extended", { prototype: p, extends: "button" });
  var div = document.createElement(div);
  div.innerHTML = '<button is="x-inner-html-extended"></button>';
  is(createdCallbackCalled, true, "created callback should be called after setting innerHTML.");
  runNextTest();
}

function testInnerHTMLUpgrade() {
  var createdCallbackCalled = false;

  var div = document.createElement(div);
  div.innerHTML = '<x-inner-html-upgrade></x-inner-html-upgrade>';

  var p = Object.create(HTMLElement.prototype);
  p.createdCallback = function() {
    is(createdCallbackCalled, false, "created callback should only be called once in this test.");
    createdCallbackCalled = true;
  };

  document.registerElement("x-inner-html-upgrade", { prototype: p });
  is(createdCallbackCalled, true, "created callback should be called after registering.");
  runNextTest();
}

function testInnerHTMLExtendedUpgrade() {
  var createdCallbackCalled = false;

  var div = document.createElement(div);
  div.innerHTML = '<button is="x-inner-html-extended-upgrade"></button>';

  var p = Object.create(HTMLButtonElement.prototype);
  p.createdCallback = function() {
    is(createdCallbackCalled, false, "created callback should only be called once in this test.");
    createdCallbackCalled = true;
  };

  document.registerElement("x-inner-html-extended-upgrade", { prototype: p, extends: "button" });
  is(createdCallbackCalled, true, "created callback should be called after registering.");
  runNextTest();
}

// Test callback when creating element after registering an element type.
// register -> create element -> insert into document -> remove from document
function testRegisterResolved() {
  var createdCallbackCalled = false;
  var attachedCallbackCalled = false;
  var detachedCallbackCalled = false;

  var createdCallbackThis;

  var p = Object.create(HTMLElement.prototype);
  p.createdCallback = function() {
    is(createdCallbackCalled, false, "Created callback should only be called once in this test.");
    createdCallbackThis = this;
    createdCallbackCalled = true;
  };

  p.attachedCallback = function() {
    is(createdCallbackCalled, true, "created callback should be called before attached callback.");
    is(attachedCallbackCalled, false, "attached callback should only be called on in this test.");
    is(this, createdElement, "The 'this' value should be the custom element.");
    attachedCallbackCalled = true;
  };

  p.detachedCallback = function() {
    is(attachedCallbackCalled, true, "attached callback should be called before detached");
    is(detachedCallbackCalled, false, "detached callback should only be called once in this test.");
    is(this, createdElement, "The 'this' value should be the custom element.");
    detachedCallbackCalled = true;
    runNextTest();
  };

  p.attributeChangedCallback = function() {
    ok(false, "attributeChanged callback should never be called in this test.");
  };

  document.registerElement("x-resolved", { prototype: p });
  is(createdCallbackCalled, false, "Created callback should not be called when custom element instance has not been created.");

  var createdElement = document.createElement("x-resolved");
  is(createdCallbackThis, createdElement, "The 'this' value in the created callback should be the custom element.");
  is(createdElement.__proto__, p, "Prototype of custom element should be the registered prototype.");

  // Insert element into document to trigger attached callback.
  container.appendChild(createdElement);

  // Remove element from document to trigger detached callback.
  container.removeChild(createdElement);
}

// Callbacks should always be the same ones when registered.
function testChangingCallback() {
  var p = Object.create(HTMLElement.prototype);
  var callbackCalled = false;
  p.attributeChangedCallback = function(name, oldValue, newValue) {
    is(callbackCalled, false, "Callback should only be called once in this test.");
    callbackCalled = true;
    runNextTest();
  };

  document.registerElement("x-test-callback", { prototype: p });

  p.attributeChangedCallback = function(name, oldValue, newValue) {
    ok(false, "Only callbacks at registration should be called.");
  };

  var elem = document.createElement("x-test-callback");
  elem.setAttribute("foo", "bar");
}

function testAttributeChanged() {
  var createdCallbackCalled = false;

  var createdElement;
  var createdCallbackThis;

  var p = Object.create(HTMLElement.prototype);
  p.createdCallback = function() {
    is(createdCallbackCalled, false, "Created callback should only be called once in this test.");
    createdCallbackThis = this;
    createdCallbackCalled = true;
  };

  // Sequence of callback arguments that we expect from attribute changed callback
  // after changing attributes values in a specific order.
  var expectedCallbackArguments = [
    // [oldValue, newValue]
    [null, "newvalue"], // Setting the attribute value to "newvalue"
    ["newvalue", "nextvalue"], // Changing the attribute value from "newvalue" to "nextvalue"
    ["nextvalue", ""], // Changing the attribute value from "nextvalue" to empty string
    ["", null], // Removing the attribute.
  ];

  p.attributeChangedCallback = function(name, oldValue, newValue) {
    is(createdCallbackCalled, true, "created callback should be called before attribute changed.");
    is(this, createdElement, "The 'this' value should be the custom element.");
    ok(expectedCallbackArguments.length > 0, "Attribute changed callback should not be called more than expected.");

    is(name, "changeme", "name arugment in attribute changed callback should be the name of the changed attribute.");

    var expectedArgs = expectedCallbackArguments.shift();
    is(oldValue, expectedArgs[0], "The old value argument should match the expected value.");
    is(newValue, expectedArgs[1], "The new value argument should match the expected value.");

    if (expectedCallbackArguments.length === 0) {
      // Done with the attribute changed callback test.
      runNextTest();
    }
  };

  document.registerElement("x-attrchange", { prototype: p });

  var createdElement = document.createElement("x-attrchange");
  is(createdCallbackThis, createdElement, "The 'this' value in the created callback should be the custom element.");
  createdElement.setAttribute("changeme", "newvalue");
  createdElement.setAttribute("changeme", "nextvalue");
  createdElement.setAttribute("changeme", "");
  createdElement.removeAttribute("changeme");
}

function testAttributeChangedExtended() {
  var p = Object.create(HTMLButtonElement.prototype);
  var callbackCalled = false;
  p.attributeChangedCallback = function(name, oldValue, newValue) {
    is(callbackCalled, false, "Callback should only be called once in this test.");
    callbackCalled = true;
    runNextTest();
  };

  document.registerElement("x-extended-attribute-change", { prototype: p, extends: "button" });

  var elem = document.createElement("button", {is: "x-extended-attribute-change"});
  elem.setAttribute("foo", "bar");
}

// Creates a custom element that is an upgrade candidate (no registration)
// and mutate the element in ways that would call callbacks for registered
// elements.
function testUpgradeCandidate() {
  var createdElement = document.createElement("x-upgrade-candidate");
  container.appendChild(createdElement);
  createdElement.setAttribute("foo", "bar");
  container.removeChild(createdElement);
  ok(true, "Nothing bad should happen when trying to mutating upgrade candidates.");
  runNextTest();
}

function testNotInDocEnterLeave() {
  var p = Object.create(HTMLElement.prototype);

  p.attached = function() {
    ok(false, "attached should not be called when not entering the document.");
  };

  p.detached = function() {
    ok(false, "leaveView should not be called when not leaving the document.");
  };

  var createdElement = document.createElement("x-destined-for-fragment");

  document.registerElement("x-destined-for-fragment", { prototype: p });

  var fragment = new DocumentFragment();
  fragment.appendChild(createdElement);
  fragment.removeChild(createdElement);

  var divNotInDoc = document.createElement("div");
  divNotInDoc.appendChild(createdElement);
  divNotInDoc.removeChild(createdElement);

  runNextTest();
}

function testEnterLeaveView() {
  var attachedCallbackCalled = false;
  var detachedCallbackCalled = false;

  var p = Object.create(HTMLElement.prototype);
  p.attachedCallback = function() {
    is(attachedCallbackCalled, false, "attached callback should only be called on in this test.");
    attachedCallbackCalled = true;
  };

  p.detachedCallback = function() {
    is(attachedCallbackCalled, true, "attached callback should be called before detached");
    is(detachedCallbackCalled, false, "detached callback should only be called once in this test.");
    detachedCallbackCalled = true;
    runNextTest();
  };

  var div = document.createElement("div");
  document.registerElement("x-element-in-div", { prototype: p });
  var customElement = document.createElement("x-element-in-div");
  div.appendChild(customElement);
  is(attachedCallbackCalled, false, "Appending a custom element to a node that is not in the document should not call the attached callback.");

  container.appendChild(div);
  container.removeChild(div);
}

var testFunctions = [
  testRegisterUnresolved,
  testRegisterUnresolvedExtended,
  testInnerHTML,
  testInnerHTMLExtended,
  testInnerHTMLUpgrade,
  testInnerHTMLExtendedUpgrade,
  testRegisterResolved,
  testAttributeChanged,
  testAttributeChangedExtended,
  testUpgradeCandidate,
  testChangingCallback,
  testNotInDocEnterLeave,
  testEnterLeaveView,
  SimpleTest.finish
];

function runNextTest() {
  if (testFunctions.length > 0) {
    var nextTestFunction = testFunctions.shift();
    nextTestFunction();
  }
}

SimpleTest.waitForExplicitFinish();

runNextTest();

</script>
</body>
</html>
